Čeština

Ovládněte React Suspense pro načítání dat. Naučte se deklarativně spravovat stavy načítání, vylepšit UX pomocí přechodů a zpracovávat chyby s Error Boundaries.

React Suspense Boundaries: Hloubkový ponor do deklarativního řízení stavů načítání

Ve světě moderního webového vývoje je vytvoření plynulého a responzivního uživatelského zážitku prvořadé. Jednou z nejtrvalejších výzev, kterým vývojáři čelí, je správa stavů načítání. Od načítání dat pro uživatelský profil až po načítání nové sekce aplikace jsou okamžiky čekání kritické. Historicky to zahrnovalo spletitou síť booleovských příznaků jako isLoading, isFetching a hasError, roztroušených po našich komponentách. Tento imperativní přístup zanáší náš kód, komplikuje logiku a je častým zdrojem chyb, jako jsou souběhové chyby (race conditions).

Vstupte do světa React Suspense. Původně byl představen pro dělení kódu (code-splitting) s React.lazy(), ale jeho schopnosti se s React 18 dramaticky rozšířily a stal se z něj mocný, prvotřídní mechanismus pro zpracování asynchronních operací, zejména načítání dat. Suspense nám umožňuje spravovat stavy načítání deklarativním způsobem, což zásadně mění způsob, jakým píšeme a přemýšlíme o našich komponentách. Místo toho, aby se naše komponenty ptaly „Načítám?“, mohou jednoduše říci: „Potřebuji tato data k vykreslení. Zatímco čekám, zobraz prosím toto záložní UI.“

Tento komplexní průvodce vás provede cestou od tradičních metod správy stavu k deklarativnímu paradigmatu React Suspense. Prozkoumáme, co jsou Suspense boundaries, jak fungují pro dělení kódu i načítání dat a jak orchestrovat komplexní načítací UI, které vaše uživatele potěší, nikoli frustruje.

Starý způsob: Dřina s manuálním řízením stavů načítání

Než plně oceníme eleganci Suspense, je nezbytné pochopit problém, který řeší. Podívejme se na typickou komponentu, která načítá data pomocí hooků useEffect a useState.

Představte si komponentu, která potřebuje načíst a zobrazit uživatelská data:


import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reset state for new userId
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Re-fetch when userId changes

  if (isLoading) {
    return <p>Loading profile...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

Tento vzor je funkční, ale má několik nevýhod:

Přichází React Suspense: Změna paradigmatu

Suspense obrací tento model vzhůru nohama. Místo toho, aby komponenta spravovala stav načítání interně, komunikuje svou závislost na asynchronní operaci přímo Reactu. Pokud data, která potřebuje, ještě nejsou k dispozici, komponenta „pozastaví“ vykreslování.

Když se komponenta pozastaví, React projde strom komponent směrem nahoru, aby našel nejbližší Suspense Boundary. Suspense Boundary je komponenta, kterou definujete ve svém stromu pomocí <Suspense>. Tato hranice pak vykreslí záložní UI (jako je spinner nebo skeleton loader), dokud všechny komponenty uvnitř ní nevyřeší své datové závislosti.

Základní myšlenkou je umístit datovou závislost společně s komponentou, která ji potřebuje, a zároveň centralizovat UI pro načítání na vyšší úrovni ve stromu komponent. To čistí logiku komponent a dává vám mocnou kontrolu nad uživatelským zážitkem při načítání.

Jak se komponenta „pozastaví“?

Kouzlo za Suspense spočívá ve vzoru, který se na první pohled může zdát neobvyklý: vyhození Promise. Datový zdroj podporující Suspense funguje takto:

  1. Když komponenta požádá o data, datový zdroj zkontroluje, zda má data v mezipaměti.
  2. Pokud jsou data k dispozici, vrátí je synchronně.
  3. Pokud data nejsou k dispozici (tj. právě se načítají), datový zdroj vyhodí Promise, který reprezentuje probíhající požadavek na načtení.

React tuto vyhozenou Promise zachytí. Neshodí vaši aplikaci. Místo toho ji interpretuje jako signál: „Tato komponenta ještě není připravena k vykreslení. Pozastav ji a hledej nad ní Suspense boundary, aby se zobrazilo záložní UI.“ Jakmile se Promise vyřeší, React se pokusí komponentu znovu vykreslit, která nyní obdrží svá data a úspěšně se vykreslí.

Hranice <Suspense>: Váš deklarátor pro UI načítání

Komponenta <Suspense> je srdcem tohoto vzoru. Její použití je neuvěřitelně jednoduché, přijímá jediný povinný prop: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Moje aplikace</h1>
      <Suspense fallback={<p>Načítání obsahu...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

V tomto příkladu, pokud se SomeComponentThatFetchesData pozastaví, uživatel uvidí zprávu „Načítání obsahu...“, dokud nebudou data připravena. Fallback může být jakýkoli platný React uzel, od jednoduchého řetězce po složitou skeleton komponentu.

Klasický případ užití: Dělení kódu (code splitting) s React.lazy()

Nejrozšířenějším použitím Suspense je dělení kódu. Umožňuje odložit načítání JavaScriptu pro komponentu, dokud není skutečně potřeba.


import React, { Suspense, lazy } from 'react';

// Kód této komponenty nebude v počátečním balíčku.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Nějaký obsah, který se načte okamžitě</h2>
      <Suspense fallback={<div>Načítání komponenty...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Zde React načte JavaScript pro HeavyComponent pouze tehdy, když se ji poprvé pokusí vykreslit. Zatímco je načítán a parsován, zobrazí se fallback ze Suspense. Toto je mocná technika pro zlepšení počáteční doby načítání stránky.

Moderní hranice: Načítání dat se Suspense

Ačkoli React poskytuje mechanismus Suspense, neposkytuje specifického klienta pro načítání dat. Abyste mohli použít Suspense pro načítání dat, potřebujete datový zdroj, který se s ním integruje (tj. takový, který vyhodí Promise, když jsou data nevyřízená).

Frameworky jako Relay a Next.js mají vestavěnou, prvotřídní podporu pro Suspense. Populární knihovny pro načítání dat jako TanStack Query (dříve React Query) a SWR také nabízejí experimentální nebo plnou podporu.

Abychom pochopili koncept, vytvořme si velmi jednoduchý, koncepční obal kolem fetch API, aby byl kompatibilní se Suspense. Poznámka: Toto je zjednodušený příklad pro vzdělávací účely a není připraven pro produkční nasazení. Chybí mu správné cachování a složitosti zpracování chyb.


// data-fetcher.js
// Jednoduchá cache pro ukládání výsledků
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Tady je to kouzlo!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Fetch failed with status ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Tento obal udržuje jednoduchý stav pro každou URL. Když je zavolána funkce fetchData, zkontroluje stav. Pokud je nevyřízený, vyhodí promise. Pokud je úspěšný, vrátí data. Nyní přepišme naši komponentu UserProfile s použitím tohoto.


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// Komponenta, která skutečně používá data
function ProfileDetails({ userId }) {
  // Pokusí se přečíst data. Pokud nejsou připravena, toto pozastaví vykreslování.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Rodičovská komponenta, která definuje UI stavu načítání
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Načítání profilu...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Podívejte se na ten rozdíl! Komponenta ProfileDetails je čistá a zaměřená pouze na vykreslování dat. Nemá žádné stavy isLoading nebo error. Jednoduše si vyžádá data, která potřebuje. Odpovědnost za zobrazení indikátoru načítání byla přesunuta nahoru na rodičovskou komponentu UserProfile, která deklarativně určuje, co se má zobrazit během čekání.

Orchestrace komplexních stavů načítání

Skutečná síla Suspense se projeví, když vytváříte komplexní UI s více asynchronními závislostmi.

Vnořené Suspense Boundaries pro postupné UI

Můžete vnořovat Suspense boundaries a vytvořit tak propracovanější zážitek z načítání. Představte si stránku s dashboardem, která má postranní panel, hlavní obsahovou oblast a seznam posledních aktivit. Každá z těchto částí může vyžadovat vlastní načtení dat.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <div className="layout">
        <Suspense fallback={<p>Načítání navigace...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

S touto strukturou:

To vám umožní ukázat uživateli užitečný obsah co nejrychleji, což dramaticky zlepšuje vnímaný výkon.

Jak se vyhnout „popcorningu“ UI

Někdy může postupný přístup vést k rušivému efektu, kdy se několik spinnerů objeví a zmizí v rychlém sledu, což se často nazývá „popcorning“. K vyřešení tohoto problému můžete posunout Suspense boundary výše ve stromu.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

V této verzi je zobrazen jediný DashboardSkeleton, dokud všechny potomkovské komponenty (Sidebar, MainContent, ActivityFeed) nemají svá data připravena. Celý dashboard se pak objeví najednou. Volba mezi vnořenými hranicemi a jedinou hranicí na vyšší úrovni je rozhodnutím o UX designu, které Suspense umožňuje triviálně implementovat.

Zpracování chyb s Error Boundaries

Suspense řeší nevyřízený (pending) stav promise, ale co zamítnutý (rejected) stav? Pokud se promise vyhozená komponentou zamítne (např. chyba sítě), bude se s ní zacházet jako s jakoukoli jinou chybou při vykreslování v Reactu.

Řešením je použít Error Boundaries. Error Boundary je class komponenta, která definuje speciální metodu životního cyklu, componentDidCatch(), nebo statickou metodu getDerivedStateFromError(). Zachycuje JavaScriptové chyby kdekoli ve svém stromu potomků, loguje tyto chyby a zobrazuje záložní UI.

Zde je jednoduchá komponenta Error Boundary:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Aktualizuje stav, aby další vykreslení ukázalo záložní UI.
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // Chybu můžete také logovat do služby pro hlášení chyb
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Můžete vykreslit jakékoli vlastní záložní UI
      return <h1>Něco se pokazilo. Zkuste to prosím znovu.</h1>;
    }

    return this.props.children; 
  }
}

Poté můžete kombinovat Error Boundaries se Suspense a vytvořit robustní systém, který zvládá všechny tři stavy: nevyřízený, úspěšný a chybný.


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>Informace o uživateli</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Načítání...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

S tímto vzorem, pokud se načítání dat uvnitř UserProfile podaří, zobrazí se profil. Pokud je nevyřízené, zobrazí se fallback ze Suspense. Pokud selže, zobrazí se fallback z Error Boundary. Logika je deklarativní, kompoziční a snadno pochopitelná.

Transitions: Klíč k neblokujícím aktualizacím UI

Zbývá poslední kousek skládačky. Zvažte interakci uživatele, která spouští nové načítání dat, jako je kliknutí na tlačítko „Další“ pro zobrazení jiného uživatelského profilu. S výše uvedeným nastavením, v okamžiku, kdy je tlačítko stisknuto a prop userId se změní, komponenta UserProfile se znovu pozastaví. To znamená, že aktuálně viditelný profil zmizí a bude nahrazen fallbackem pro načítání. To může působit náhle a rušivě.

Zde přicházejí na řadu transitions (přechody). Transitions jsou novou funkcí v React 18, která vám umožní označit určité aktualizace stavu jako neurgentní. Když je aktualizace stavu zabalena do transition, React bude nadále zobrazovat staré UI (zastaralý obsah), zatímco v pozadí připravuje nový obsah. Aktualizaci UI provede až ve chvíli, kdy je nový obsah připraven k zobrazení.

Hlavním API pro toto je hook useTransition.


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Další uživatel
      </button>

      {isPending && <span> Načítání nového profilu...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Načítání počátečního profilu...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Co se nyní stane:

  1. Načte se počáteční profil pro userId: 1, zobrazí se fallback ze Suspense.
  2. Uživatel klikne na „Další uživatel“.
  3. Volání setUserId je zabaleno do startTransition.
  4. React začne v paměti vykreslovat UserProfile s novým userId 2. To způsobí jeho pozastavení.
  5. Klíčové je, že místo zobrazení fallbacku ze Suspense, React ponechá na obrazovce staré UI (profil uživatele 1).
  6. Booleovská hodnota isPending vrácená z useTransition se stane true, což nám umožňuje zobrazit decentní, inline indikátor načítání bez odmontování starého obsahu.
  7. Jakmile jsou data pro uživatele 2 načtena a UserProfile se může úspěšně vykreslit, React provede aktualizaci a nový profil se plynule objeví.

Transitions poskytují poslední vrstvu kontroly, která vám umožňuje vytvářet sofistikované a uživatelsky přívětivé zážitky z načítání, které nikdy nepůsobí rušivě.

Doporučené postupy a globální souvislosti

Závěr

React Suspense představuje více než jen novou funkci; je to zásadní evoluce v tom, jak přistupujeme k asynchronicitě v React aplikacích. Tím, že se odkloníme od manuálních, imperativních příznaků načítání a přijmeme deklarativní model, můžeme psát komponenty, které jsou čistší, odolnější a snáze se skládají.

Kombinací <Suspense> pro nevyřízené stavy, Error Boundaries pro chybové stavy a useTransition pro plynulé aktualizace máte k dispozici kompletní a mocnou sadu nástrojů. Můžete orchestrovat vše od jednoduchých načítacích spinnerů po komplexní, postupné odhalování dashboardů s minimálním a předvídatelným kódem. Jakmile začnete integrovat Suspense do svých projektů, zjistíte, že nejen zlepšuje výkon a uživatelský zážitek vaší aplikace, ale také dramaticky zjednodušuje vaši logiku správy stavu, což vám umožní soustředit se na to, na čem skutečně záleží: na budování skvělých funkcí.